import asyncio
import hashlib
import json
import logging
from datetime import datetime
from functools import partial

from pymobiledevice3.services.web_protocol.cdp_screencast import ScreenCast

logger = logging.getLogger(__name__)

NETWORK_RESOURCE_TYPES = [
    "Document",
    "Stylesheet",
    "Image",
    "Media",
    "Font",
    "Script",
    "TextTrack",
    "XHR",
    "Fetch",
    "EventSource",
    "WebSocket",
    "Manifest",
    "SignedExchange",
    "Ping",
    "CSPViolationReport",
    "Preflight",
    "Other",
]

LOG_MESSAGE_SOURCES = {
    "xml": "xml",
    "javascript": "javascript",
    "network": "network",
    "console-api": "javascript",
    "storage": "storage",
    "appcache": "appcache",
    "rendering": "rendering",
    "css": "rendering",
    "security": "security",
    "deprecation": "deprecation",
    "worker": "worker",
    "violation": "violation",
    "intervention": "intervention",
    "recommendation": "recommendation",
    "other": "other",
    "content-blocker": "other",
    "media": "other",
    "mediasource": "other",
    "webrtc": "other",
    "itp-debug": "other",
    "ad-click-attribution": "other",
}

LOG_MESSAGE_LEVELS = {
    "log": "info",
    "info": "info",
    "warning": "warning",
    "error": "error",
    "debug": "verbose",
}

DEBUGGER_PAUSED_REASON = {
    "XHR": "XHR",
    "Fetch": "other",
    "DOM": "DOM",
    "AnimationFrame": "other",
    "Interval": "other",
    "Listener": "EventListener",
    "Timeout": "other",
    "exception": "exception",
    "assert": "assert",
    "CSPViolation": "CSPViolation",
    "DebuggerStatement": "debugCommand",
    "Breakpoint": "instrumentation",
    "PauseOnNextStatement": "instrumentation",
    "Microtask": "other",
    "BlackboxedScript": "other",
    "other": "other",
}


class CdpTarget:
    def __init__(self, protocol, target_id: str):
        """
        :param pymobiledevice3.services.web_protocol.session_protocol.SessionProtocol protocol: Session protocol.
        :param target_id: Target id.
        """
        self.protocol = protocol
        self.target_id = target_id
        self.frame = {}
        self.session_id = protocol.id_
        self.app_id = protocol.app.id_
        self.page_id = protocol.page.id_
        self.output_queue = asyncio.Queue()
        self.input_queue = asyncio.Queue()
        self.screencast = None
        self.from_cdp_special_messages_methods = {
            "Audits.enable": self._audits_enable,
            "DOM.getBoxModel": self._dom_get_box_model,
            "DOM.enable": partial(self._simple_response, value=None),
            "DOM.getNodeForLocation": self._dom_get_node_for_location,
            "DOM.getNodesForSubtreeByStyle": self._dom_get_nodes_for_subtree_by_style,
            "Log.clear": self._log_clear,
            "Log.disable": self._log_disable,
            "Log.enable": self._log_enable,
            "Log.startViolationsReport": partial(self._simple_response, value=None),
            "Page.getNavigationHistory": self._page_get_navigation_history,
            "Page.startScreencast": self._page_start_screencast,
            "Page.stopScreencast": self._page_stop_screencast,
            "Page.screencastFrameAck": self._page_screencast_frame_ack,
            "Page.getResourceTree": self._page_get_resource_tree,
            "Emulation.setEmulatedMedia": self._emulation_set_emulated_media,
            "Emulation.setTouchEmulationEnabled": partial(self._simple_response, value=None),
            "Emulation.setFocusEmulationEnabled": partial(self._simple_response, value=None),
            "Emulation.setEmulatedVisionDeficiency": partial(self._simple_response, value=None),
            "Emulation.setAutoDarkModeOverride": self._emulation_set_auto_dark_mode_override,
            "Emulation.setEmitTouchEventsForMouse": partial(self._simple_response, value=None),
            "Debugger.setAsyncCallStackDepth": partial(self._simple_response, value=True),
            "Debugger.setBlackboxPatterns": self._debugger_set_blackbox_patterns,
            "Debugger.setBreakpointByUrl": self._debugger_set_breakpoint_by_url,
            "DOMDebugger.setBreakOnCSPViolation": partial(self._simple_response, value=None),
            "DOMDebugger.getEventListeners": self._domdebugger_get_event_listeners,
            "Network.setCacheDisabled": self._network_set_cache_disabled,
            "Network.loadNetworkResource": self._network_load_network_resource,
            "Network.setAttachDebugStack": partial(self._simple_response, value=None),
            "Network.clearAcceptedEncodingsOverride": partial(self._simple_response, value=None),
            "ServiceWorker.enable": self._service_worker_enable,
            "HeapProfiler.enable": partial(self._simple_response, value=None),
            "Overlay.setShowGridOverlays": partial(self._simple_response, value=None),
            "Overlay.setShowFlexOverlays": partial(self._simple_response, value=None),
            "Overlay.setShowScrollSnapOverlays": partial(self._simple_response, value=None),
            "Overlay.setShowContainerQueryOverlays": partial(self._simple_response, value=None),
            "Overlay.setShowIsolatedElements": partial(self._simple_response, value=None),
            "Overlay.hideHighlight": partial(self._simple_response, value=None),
            "Overlay.highlightNode": self._overlay_highlight_node,
            "Overlay.enable": partial(self._simple_response, value=None),
            "Overlay.setShowViewportSizeOnResize": partial(self._simple_response, value=None),
            "Overlay.setPausedInDebuggerMessage": partial(self._simple_response, value=None),
            "Runtime.runIfWaitingForDebugger": partial(self._simple_response, value=None),
            "Runtime.compileScript": self._runtime_compile_script,
            "Runtime.getIsolateId": self._runtime_get_isolate_id,
            "Profiler.enable": partial(self._simple_response, value=None),
            "Target.setAutoAttach": self._target_set_auto_attach,
            "Target.setDiscoverTargets": partial(self._simple_response, value=None),
            "Target.setRemoteLocations": partial(self._simple_response, value=None),
            "CSS.trackComputedStyleUpdates": partial(self._simple_response, value=None),
            "CSS.takeComputedStyleUpdates": self._css_take_computed_style_updates,
            "CSS.addRule": self._css_add_rule,
            "Input.emulateTouchFromMouseEvent": self._input_emulate_touch_from_mouse_event,
            "Input.dispatchKeyEvent": self._input_dispatch_key_event,
        }
        self.to_cdp_special_messages_methods = {
            "Target.targetCreated": self._target_created,
            "Target.targetDestroyed": self._target_destroyed,
            "Target.dispatchMessageFromTarget": self._target_dispatch_message_from_target,
            "Target.didCommitProvisionalTarget": self._target_did_commit_provisional_target,
        }
        self.to_cdp_special_dispatched_messages_methods = {
            "Debugger.scriptParsed": self._debugger_script_parsed,
            "Debugger.scriptFailedToParse": self._debugger_script_failed_to_parse,
            "Debugger.paused": self._debugger_paused,
            "Debugger.globalObjectCleared": self._debugger_global_object_cleared,
            "Page.defaultAppearanceDidChange": self._page_default_appearance_did_change,
            "Runtime.executionContextCreated": self._runtime_execution_context_created,
            "Console.messageAdded": self._console_message_added,
            "Network.responseReceived": self._network_response_received,
            "Network.loadingFinished": self._network_loading_finished,
        }
        self._waiting_for_id = 0
        self._input_task = asyncio.create_task(self._input_loop())
        self._receiving_task = asyncio.create_task(self._receive_loop())
        self._script_source_to_context_id = {}
        self._default_execution_id = 0

    @classmethod
    async def create(cls, protocol):
        """
        :param pymobiledevice3.services.web_protocol.session_protocol.SessionProtocol protocol: Session protocol.
        """
        await protocol.inspector.setup_inspector_socket(protocol.id_, protocol.app.id_, protocol.page.id_)
        while not protocol.inspector.wir_events:
            await asyncio.sleep(0)
        created = protocol.inspector.wir_events.pop(0)
        while "targetInfo" not in created["params"]:
            created = protocol.inspector.wir_events.pop(0)
        target_id = created["params"]["targetInfo"]["targetId"]
        logger.info(f"Created: {target_id}")
        target = cls(protocol, target_id)
        await target.output_queue.put(created)
        return target

    async def send(self, message):
        """
        Send message from devtools to the target.
        """
        await self.input_queue.put(message)

    async def receive(self):
        """
        Get message from the target to the devtools.
        """
        return await self.output_queue.get()

    async def wait_for_event_id(self, id_: int):
        """
        Wait for a message with a specific id from the target.
        :param id_: Message id to wait for.
        """
        while True:
            for i in range(len(self.protocol.inspector.wir_events)):
                message = self.protocol.inspector.wir_events[i]
                if message["method"] != "Target.dispatchMessageFromTarget":
                    continue
                message = json.loads(message["params"]["message"])
                if message.get("id", "") != id_:
                    continue
                del self.protocol.inspector.wir_events[i]
                return message
            await asyncio.sleep(0)

    async def send_message_with_result(self, id_, method, params):
        """
        Send a message to the target and wait for response.
        """
        self._waiting_for_id += 1
        await self._send_message_to_target({"id": id_, "method": method, "params": params})
        result = await self.wait_for_event_id(id_)
        self._waiting_for_id -= 1
        return result

    async def evaluate_and_result(self, id_, expression):
        """
        Evaluate Javascript expression.
        """
        logger.debug("Evaluating: ", expression)
        params = {"expression": expression}
        data = await self.send_message_with_result(id_, "Runtime.evaluate", params)
        logger.debug("Evaluated: ", data)
        result = data["result"]["result"]
        if result["type"] == "string":
            return result["value"]
        elif result["type"] == "undefined":
            return None
        elif result["type"] == "object":
            return result
        else:
            logger.debug("Unknown type: ", result)
            return result

    async def _input_loop(self):
        while True:
            message = await self.input_queue.get()
            if message["method"] in self.from_cdp_special_messages_methods:
                await self.from_cdp_special_messages_methods[message["method"]](message)
            else:
                await self._send_message_to_target(message)

    async def _receive_loop(self):
        while True:
            if self._waiting_for_id or not self.protocol.inspector.wir_events:
                await asyncio.sleep(0)
                continue
            message = self.protocol.inspector.wir_events.pop(0)
            await self._to_output_queue(message)

    async def _to_output_queue(self, message):
        if message["method"] in self.to_cdp_special_messages_methods:
            await self.to_cdp_special_messages_methods[message["method"]](message)
        else:
            logger.error("Error!!!!!!!!!!!!", message)
            raise RuntimeError()

    async def _send_message_to_target(self, message):
        await self.protocol.send_command(
            "Target.sendMessageToTarget", targetId=self.target_id, message=json.dumps(message)
        )

    async def _simple_response(self, message, value):
        await self.output_queue.put({"id": message["id"], "result": {"result": value}})

    async def _audits_enable(self, message):
        message["method"] = "Audit.setup"
        await self._send_message_to_target(message)

    async def _dom_get_box_model(self, message):
        message["method"] = "DOM.highlightNode"
        message["params"]["highlightConfig"] = {
            "showInfo": True,
            "contentColor": {"r": 111, "g": 168, "b": 220, "a": 0.66},
            "paddingColor": {"r": 147, "g": 196, "b": 125, "a": 0.55},
            "borderColor": {"r": 255, "g": 229, "b": 153, "a": 0.66},
            "marginColor": {"r": 246, "g": 178, "b": 107, "a": 0.66},
        }
        await self._send_message_to_target(message)

    async def object_id_to_node_id(self, object_id, id_):
        node = await self.send_message_with_result(id_, "DOM.requestNode", {"objectId": object_id})
        return node["result"]["nodeId"]

    async def _dom_get_node_for_location(self, message):
        x, y = message["params"]["x"], message["params"]["y"]
        obj = await self.evaluate_and_result(message["id"], f"document.elementFromPoint({x},{y})")
        if "objectId" not in obj:
            await self._simple_response(message, None)
            return
        result = {"nodeId": await self.object_id_to_node_id(obj["objectId"], message["id"])}
        await self.output_queue.put({"id": message["id"], "result": result})

    async def _dom_get_nodes_for_subtree_by_style(self, message):
        object_id = (
            await self.send_message_with_result(
                message["id"], "DOM.resolveNode", {"nodeId": message["params"]["nodeId"]}
            )
        )["result"]["object"]["objectId"]
        result = await self.send_message_with_result(
            message["id"],
            "Runtime.callFunctionOn",
            {
                "objectId": object_id,
                "functionDeclaration": (
                    "function(styles) {"
                    "   const result = new Set();"
                    '   var all = this.getElementsByTagName("*");'
                    "   for (var elem_i=0; elem_i < all.length; elem_i++) {"
                    "       for (var style_i in styles) {"
                    "           if (window.getComputedStyle(all[elem_i]).getPropertyValue(styles[style_i].name) ==="
                    " styles[style_i].value) {"
                    "               result.add(all[elem_i]);"
                    "               break;"
                    "           }"
                    "       }"
                    "   }"
                    "   return result;"
                    "}"
                ),
                "arguments": [{"value": message["params"]["computedStyles"]}],
            },
        )
        result = await self.send_message_with_result(
            message["id"], "Runtime.getCollectionEntries", {"objectId": result["result"]["result"]["objectId"]}
        )
        nodes = await asyncio.gather(
            *[
                self.object_id_to_node_id(obj["value"]["objectId"], message["id"] - i)
                for i, obj in enumerate(result["result"]["entries"])
            ],
            return_exceptions=True,
        )
        nodes = [n for n in nodes if isinstance(n, int)]
        await self.output_queue.put({"id": message["id"], "result": {"nodeIds": nodes}})

    async def _log_clear(self, message):
        message["method"] = "Console.clearMessages"
        await self._send_message_to_target(message)

    async def _log_disable(self, message):
        message["method"] = "Console.disable"
        await self._send_message_to_target(message)

    async def _log_enable(self, message):
        message["method"] = "Console.enable"
        await self._send_message_to_target(message)

    async def _page_get_navigation_history(self, message):
        href = await self.evaluate_and_result(message["id"], "window.location.href")
        title = await self.evaluate_and_result(message["id"], "document.title")
        await self.output_queue.put({
            "id": message["id"],
            "result": {"currentIndex": 0, "entries": [{"id": 0, "url": href, "title": title}]},
        })

    async def _page_start_screencast(self, message):
        params = message["params"]
        self.screencast = ScreenCast(self, params["format"], params["quality"], params["maxWidth"], params["maxHeight"])
        await self.screencast.start(message["id"])
        await self._simple_response(message, None)

    async def _page_stop_screencast(self, message):
        if self.screencast is not None:
            await self.screencast.clear()
            self.screencast = None
        await self._simple_response(message, None)

    async def _page_screencast_frame_ack(self, message):
        if self.screencast is not None:
            self.screencast.ack(message["params"]["sessionId"])
        await self._simple_response(message, None)

    async def _page_get_resource_tree(self, message):
        result = await self.send_message_with_result(message["id"], message["method"], message["params"])
        self.frame = result["result"]["frameTree"]["frame"]
        await self.output_queue.put(result)

    async def _emulation_set_emulated_media(self, message):
        message["method"] = "Page.setEmulatedMedia"
        await self._send_message_to_target(message)

    async def _emulation_set_auto_dark_mode_override(self, message):
        message["method"] = "Page.setForcedAppearance"
        params = message["params"]
        if not params:
            await self._simple_response(message, None)
            return
        message["params"] = {"appearance": "Dark" if params["enabled"] else "Light"}
        await self._send_message_to_target(message)

    async def _debugger_set_blackbox_patterns(self, message):
        for pattern in message["params"]["patterns"]:
            await self.send_message_with_result(
                message["id"], "Debugger.setShouldBlackboxURL", {"url": pattern, "shouldBlackbox": True}
            )
        await self._simple_response(message, None)

    async def _debugger_set_breakpoint_by_url(self, message):
        condition = message["params"].pop("condition", "")
        if condition:
            message["params"]["options"]["condition"] = condition
        await self._send_message_to_target(message)

    async def _domdebugger_get_event_listeners(self, message):
        node = {"nodeId": await self.object_id_to_node_id(message["params"]["objectId"], message["id"])}
        listeners = await self.send_message_with_result(message["id"], "DOM.getEventListenersForNode", node)
        if "error" in listeners:
            await self._simple_response(message, None)
            return
        listeners_out = []
        for listener in listeners["result"]["listeners"]:
            data = {
                "type": listener["type"],
                "useCapture": listener["useCapture"],
                "passive": listener.get("passive", False),
                "once": listener.get("once", False),
            }
            if "location" in listener:
                data["scriptId"] = listener["location"]["scriptId"]
                data["lineNumber"] = listener["location"]["lineNumber"]
                data["columnNumber"] = listener["location"]["columnNumber"]
            listeners_out.append(data)
        await self.output_queue.put({"id": message["id"], "result": {"listeners": listeners_out}})

    async def _network_set_cache_disabled(self, message):
        message["method"] = "Network.setResourceCachingDisabled"
        message["params"] = {"disabled": message["params"]["cacheDisabled"]}
        await self._send_message_to_target(message)

    async def _network_load_network_resource(self, message):
        await self.output_queue.put({"id": message["id"], "result": {"resource": {"success": True}}})

    async def _service_worker_enable(self, message):
        message["method"] = "Worker.enable"
        await self._send_message_to_target(message)

    async def _overlay_highlight_node(self, message):
        message["method"] = "DOM.highlightNode"
        await self._send_message_to_target(message)

    async def _runtime_compile_script(self, message):
        self._script_source_to_context_id[message["params"]["expression"]] = message["params"]["executionContextId"]
        response = await self.send_message_with_result(
            message["id"], "Runtime.parse", {"source": message["params"]["expression"]}
        )
        if response["result"]["result"] == "none":
            await self._simple_response(message, None)
            return
        lines = message["params"]["expression"][: response["result"]["range"]["endOffset"]].splitlines()
        lines = lines if lines else [""]
        await self.output_queue.put({
            "id": message["id"],
            "result": {
                "exceptionDetails": {
                    "exceptionId": 1,
                    "text": response["result"]["message"],
                    "lineNumber": len(lines) - 1,
                    "columnNumber": len(lines[-1]) - 1,
                }
            },
        })

    async def _runtime_get_isolate_id(self, message):
        await self.output_queue.put({"id": message["id"], "result": {"id": self._default_execution_id}})

    async def _target_set_auto_attach(self, message):
        await self._simple_response(message, None)
        await self.output_queue.put({
            "method": "Target.attachedToTarget",
            "params": {
                "sessionId": self.protocol.id_,
                "targetInfo": {"targetId": self.target_id},
                "waitingForDebugger": True,
            },
        })

    async def _css_take_computed_style_updates(self, message):
        await self.output_queue.put({"id": message["id"], "result": {"nodeIds": []}})

    async def _css_add_rule(self, message):
        message["params"]["selector"] = message["params"]["ruleText"].split("{")[0]
        await self._send_message_to_target(message)

    async def _input_emulate_touch_from_mouse_event(self, message):
        params = message["params"]
        if params["type"] == "mouseWheel":
            delta_x, delta_y = params["deltaX"] // self.screencast.scale, params["deltaY"] // self.screencast.scale
            await self.evaluate_and_result(message["id"], f"window.scrollBy({-delta_x}, {-delta_y})")
        elif params["type"] == "mouseReleased":
            pass
        else:
            modifiers = params["modifiers"]
            x, y = params["x"] // self.screencast.scale, params["y"] // self.screencast.scale
            event_params = {
                "screenX": x,
                "screenY": y,
                "clientX": 0,
                "clientY": 0,
                "altKey": bool(modifiers & 1),
                "ctrlKey": bool(modifiers & 2),
                "metaKey": bool(modifiers & 4),
                "shiftKey": bool(modifiers & 8),
                "button": params["button"],
                "bubbles": True,
                "cancelable": False,
            }
            type_ = {"mousePressed": "click", "mouseReleased": "click", "mouseMoved": "mousemove"}[params["type"]]
            event_params = json.dumps(event_params)
            simulate_mouse_event = (
                "function simulateMouseEvent(type){"
                f"const element = document.elementFromPoint({x}, {y});"
                f"const e = new MouseEvent(type, JSON.parse('{event_params}'));"
                "element.dispatchEvent(e);"
                "element.focus();"
                "return e;}"
            )
            await self.evaluate_and_result(message["id"], f'({simulate_mouse_event})("{type_}")')
            if type_ == "click":
                await self.evaluate_and_result(message["id"], f'({simulate_mouse_event})("mouseup")')

        await self._simple_response(message, None)

    async def _input_dispatch_key_event(self, message):
        params = message["params"]
        key = params["key"]
        if params["type"] == "keyUp" and key == "Backspace":
            manipulation = "document.activeElement.value = document.activeElement.value.slice(0, -1);"
        elif params["type"] == "char" and key == "Enter":
            manipulation = (
                "var tagName = document.activeElement.tagName.toLowerCase();"
                'if (tagName === "textarea" || document.activeElement.isContentEditable) {'
                '    document.activeElement.value = document.activeElement.value + "\\n";'
                "} else {"
                '    const result = document.evaluate("./ancestor-or-self::form", document.activeElement, '
                "                                     null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);"
                "    const e = result.singleNodeValue.ownerDocument.createEvent('Event');"
                '    e.initEvent("submit", true, true);'
                "    if (result.singleNodeValue.dispatchEvent(e)) { result.singleNodeValue.submit() }"
                "}"
            )
        elif params["type"] == "char":
            text = params["text"]
            manipulation = f'document.activeElement.value = document.activeElement.value + "{text}";'
        else:
            await self._simple_response(message, None)
            return

        simulate_key_event = (
            "function isEditable(element) {"
            "    if (element.disabled || element.readOnly)"
            "        return false;"
            "    var tagName = element.tagName.toLowerCase();"
            '    if (tagName === "textarea" || element.isContentEditable)'
            "        return true;"
            '    if (tagName != "input")'
            "        return false;"
            "    switch (element.type) {"
            '    case "color": case "date": case "datetime-local": case "email": case "file": case "month": '
            '    case "number": case "password": case "range": case "search": case "tel": case "text": case "time": '
            '    case "url": case "week":'
            "        return true;"
            "    }"
            "    return false;"
            "}"
            "if (isEditable(document.activeElement)) {"
            f"{manipulation}"
            "}"
        )
        await self.evaluate_and_result(message["id"], simulate_key_event)
        await self._simple_response(message, None)

    async def _target_created(self, message):
        self.target_id = message["params"]["targetInfo"]["targetId"]
        message["method"] = "Target.targetInfoChanged"
        message["params"]["targetInfo"]["url"] = await self.evaluate_and_result(1, "window.location.href")
        message["params"]["targetInfo"]["title"] = await self.evaluate_and_result(1, "document.title")
        message["params"]["targetInfo"]["attached"] = message["params"]["targetInfo"].pop("isProvisional")
        await self.output_queue.put(message)

    async def _target_destroyed(self, message):
        result = await self.send_message_with_result(1, "Page.getResourceTree", {})
        self.frame = result["result"]["frameTree"]["frame"]
        await self.output_queue.put({
            "method": "Page.frameNavigated",
            "params": {
                "frame": self.frame,
            },
        })
        await self.output_queue.put({
            "method": "Page.loadEventFired",
            "params": {
                "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
            },
        })
        await self.output_queue.put({"method": "DOM.documentUpdated"})

    async def _target_dispatch_message_from_target(self, message):
        message = json.loads(message["params"]["message"])
        if "error" in message:
            logger.error(message)
        if message.get("method", "") in self.to_cdp_special_dispatched_messages_methods:
            await self.to_cdp_special_dispatched_messages_methods[message["method"]](message)
        else:
            if "method" in message:
                logger.debug("DISPACHING", message["method"])
            await self.output_queue.put(message)

    async def _target_did_commit_provisional_target(self, message):
        pass

    async def _debugger_script_parsed(self, message):
        if not self._waiting_for_id:
            await self.output_queue.put(message)

    async def _debugger_script_failed_to_parse(self, message):
        message["params"] = {
            "endColumn": 0,
            "endLine": message["params"]["errorLine"],
            "executionContextId": self._script_source_to_context_id[message["params"]["scriptSource"]],
            "startColumn": 0,
            "startLine": message["params"]["startLine"],
            "url": message["params"]["url"],
            "scriptId": self._script_source_to_context_id[message["params"]["scriptSource"]],
            "hash": hashlib.sha1(message["params"]["scriptSource"]).hexdigest(),
        }
        await self.output_queue.put(message)

    async def _debugger_paused(self, message):
        message["params"]["reason"] = DEBUGGER_PAUSED_REASON[message["params"]["reason"]]
        if "breakpointId" in message["params"].get("data", {}):
            message["params"]["hitBreakpoints"] = [message["params"]["data"]["breakpointId"]]
        await self.output_queue.put(message)

    async def _debugger_global_object_cleared(self, message):
        await self.output_queue.put({"method": "DOM.documentUpdated"})

    async def _page_default_appearance_did_change(self, message):
        pass

    async def _runtime_execution_context_created(self, message):
        if message["params"]["context"]["type"] == "normal":
            self._default_execution_id = message["params"]["context"]["id"]
        message["params"] = {
            "context": {
                "id": message["params"]["context"]["id"],
                "origin": "default",
                "name": "",
                "uniqueId": message["params"]["context"]["frameId"],
            }
        }
        await self.output_queue.put(message)

    async def _console_message_added(self, message):
        log_record = {
            "source": LOG_MESSAGE_SOURCES[message["params"]["message"]["source"]],
            "level": LOG_MESSAGE_LEVELS[message["params"]["message"]["level"]],
            "text": message["params"]["message"]["text"],
            "timestamp": datetime.now().timestamp(),
        }
        if "url" in message["params"]["message"]:
            log_record["url"] = message["params"]["message"]["url"]
        if "line" in message["params"]["message"]:
            log_record["lineNumber"] = message["params"]["message"]["line"]
        if "networkRequestId" in message["params"]["message"]:
            log_record["networkRequestId"] = message["params"]["message"]["networkRequestId"]

        await self.output_queue.put({"method": "Log.entryAdded", "params": {"entry": log_record}})

    async def _network_response_received(self, message):
        params = message["params"]
        message["params"] = {
            "loaderId": params["loaderId"],
            "requestId": params["requestId"],
            "timestamp": params["timestamp"],
            "type": params["type"] if params["type"] in NETWORK_RESOURCE_TYPES else "Other",
            "response": {
                "url": params["response"]["url"],
                "status": params["response"]["status"],
                "statusText": params["response"]["statusText"],
                "headers": params["response"]["headers"],
                "mimeType": params["response"]["mimeType"],
                "connectionReused": False,
                "encodedDataLength": 0,
                "securityState": "unknown",
            },
        }
        if "frameId" in params:
            message["params"]["frameId"] = params["frameId"]
        await self.output_queue.put(message)

    async def _network_loading_finished(self, message):
        params = message["params"]
        header_size = params["metrics"].get("responseHeaderBytesReceived", 0)
        body_size = params["metrics"].get("responseBodyBytesReceived", 0)
        message["params"] = {
            "encodedDataLength": header_size + body_size,
            "requestId": params["requestId"],
            "timestamp": params["timestamp"],
        }
        await self.output_queue.put(message)
